/*  Copyright (c) 2012 William Rogers and James Boud

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

using System;
using SlimDX;
using SlimDX.Direct3D11;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using JBBRXG11ContentTextureCube;

namespace JBBRXG11
{
    /// <summary>
    /// Represents a set of six 2D textures, one for each face of a cube.
    /// </summary>
    public class TextureCube : ContentTypeReader<TextureCube>, IDisposable
    {
        protected SlimDX.Direct3D11.Texture2D dxtexture;
        ShaderResourceView dxtextureview;
        int size;

        //////////////////////////////////////////////////////////////////////////////////////////////////////////////
        /////////////////////////////// Constructors and Content Reader Code//////////////////////////////////////////
        //////////////////////////////////////////////////////////////////////////////////////////////////////////////

        protected override TextureCube Read(ContentReader input, TextureCube existingInstance)
        {
            TextureCubeInfoStuff tcis = new JBBRXG11ContentTextureCube.TextureCubeInfoStuff();
            tcis.size = input.ReadInt32();
            tcis.faces = new JBBRXG11ContentTexture.Texture2DInfoStuff[6];
            for (int f = 0; f < 6; f++)
            {
                JBBRXG11ContentTexture.Texture2DInfoStuff tis = new JBBRXG11ContentTexture.Texture2DInfoStuff();
                int chainlen, datalen;
                chainlen = input.ReadInt32();
                tis.mipchain = new JBBRXG11ContentTexture.TextureBitmap[chainlen];
                for (int m = 0; m < chainlen; m++)
                {
                    tis.mipchain[m].width = input.ReadInt32();
                    tis.mipchain[m].height = input.ReadInt32();
                    int format = input.ReadInt32();
                    tis.mipchain[m].format = (Microsoft.Xna.Framework.Graphics.SurfaceFormat)(format);
                    datalen = input.ReadInt32();
                    tis.mipchain[m].data = input.ReadBytes(datalen);
                }
                tcis.faces[f] = tis;
            }

            // Can't use 'this' instance as it belongs to the content manager which may reuse it, overwriting this object
            TextureCube newtex = new TextureCube();
            newtex.ProcessInfo(Game.GraphicsDevice, tcis);
            GraphicsDeviceManager.GetGame(Game.GraphicsDevice).UserContentAdd(newtex);
            return newtex;
        }

        void ProcessInfo(GraphicsDevice device, TextureCubeInfoStuff tcis)
        {
            Texture2DDescription desc;

            desc = new Texture2DDescription();
            desc.ArraySize = 6;
            desc.BindFlags = BindFlags.ShaderResource | BindFlags.RenderTarget;
            desc.CpuAccessFlags = CpuAccessFlags.None;
            if (tcis.faces[0].mipchain[0].format != Microsoft.Xna.Framework.Graphics.SurfaceFormat.Color)
                throw new Exception("JBBRXG11(NYI): Texture2D can only cope with Color surface format");
            desc.Format = SlimDX.DXGI.Format.R8G8B8A8_UNorm;// tis.mipchain[0].format;  todo translate formats
            size = tcis.size;
            desc.Height = size;
            desc.Width = size;
            desc.MipLevels = tcis.faces[0].mipchain.Length;
            desc.OptionFlags = ResourceOptionFlags.TextureCube;
            desc.SampleDescription = new SlimDX.DXGI.SampleDescription(1, 0);
            desc.Usage = ResourceUsage.Default;

            DataRectangle[] data = new DataRectangle[desc.MipLevels * 6];
            for (int f = 0; f < 6; f++)
            {
                for (int m = 0; m < desc.MipLevels; m++)
                {
                    data[f * desc.MipLevels + m] = new DataRectangle(tcis.faces[f].mipchain[m].width * 4,
                                                                     new DataStream(tcis.faces[f].mipchain[m].data, true, false));
                }
            }

            dxtexture = new SlimDX.Direct3D11.Texture2D(device.DX_Device, desc, data);
            dxtextureview = new ShaderResourceView(device.DX_Device, dxtexture);
        }

        void MakeTextureCube(GraphicsDevice graphicsDevice, int size, bool mipMap, SurfaceFormat format, bool couldberendertarget)
        {
            Texture2DDescription desc;
            desc = new Texture2DDescription();
            desc.ArraySize = 6;
            if (couldberendertarget)
                desc.BindFlags = BindFlags.ShaderResource | BindFlags.RenderTarget;
            else
                desc.BindFlags = BindFlags.ShaderResource;
            desc.CpuAccessFlags = CpuAccessFlags.None;
            if (format != Microsoft.Xna.Framework.Graphics.SurfaceFormat.Color)
                throw new Exception("JBBRXG11(NYI): TextureCube can only cope with Color surface format");
            desc.Format = EnumConversions.convert(format);  // SlimDX.DXGI.Format.R8G8B8A8_UNorm; // tis.mipchain[0].format;  todo translate formats
            this.size = size;
            desc.Height = size;
            desc.Width = size;
            desc.MipLevels = (mipMap ? 0 : 1);
            desc.OptionFlags = ResourceOptionFlags.TextureCube;
            desc.SampleDescription = new SlimDX.DXGI.SampleDescription(1, 0);
            desc.Usage = ResourceUsage.Default;

            dxtexture = new SlimDX.Direct3D11.Texture2D(graphicsDevice.DX_Device, desc);
            dxtextureview = new ShaderResourceView(graphicsDevice.DX_Device, dxtexture);
        }

        /// <summary>
        /// Creates a new instance of this object.
        /// </summary>
        /// <param name="graphicsDevice">The device.</param>
        /// <param name="size">The size (in pixels) of the top-level faces of the
        /// cube texture. Subsequent levels of each face will be the truncated value
        /// of half of the previous level's pixel dimension (independently). Each
        /// dimension is clamped to a minimum of 1 pixel.</param>
        /// <param name="mipMap">[MarshalAsAttribute(U1)] True to generate a full
        /// mipmap chain, false otherwise.</param>
        /// <param name="format">Surface data format.</param>
        public TextureCube(GraphicsDevice graphicsDevice, int size, bool mipMap, SurfaceFormat format)
        {
            MakeTextureCube(graphicsDevice, size, mipMap, format, false);
            GraphicsDeviceManager.GetGame(Game.GraphicsDevice).UserContentAdd(this);
        }

        protected TextureCube(GraphicsDevice graphicsDevice, int size, bool mipMap, SurfaceFormat format, bool couldberendertarget)
        {
            MakeTextureCube(graphicsDevice, size, mipMap, format, couldberendertarget);
        }

        /// <summary>
        /// Do not use this constructor, required by the content system
        /// </summary>
        public TextureCube() // Constructor for content system to use to before calling Read - not in XNA API
        {
            dxtexture = null;
            dxtextureview = null;
        }

        //////////////////////////////////////////////////////////////////////////////////////////////////////////////
        //////////////////////////////////////// Properties //////////////////////////////////////////////////////////
        //////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal ShaderResourceView View { get { return dxtextureview; } }

        /// <summary>
        /// Gets the width and height of this texture resource, in pixels.
        /// </summary>
        public int Size { get { return size; } }

        //////////////////////////////////////////////////////////////////////////////////////////////////////////////
        //////////////////////////////////////// Methods /////////////////////////////////////////////////////////////
        //////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>
        /// Sets cube texture data, specifying a cubemap face.
        /// </summary>
        /// <param name="cubeMapFace">The cubemap face.</param>
        /// <param name="data">Array of data.</param>
        public void SetData<T>(CubeMapFace cubeMapFace, T[] data) where T : struct
        {
            SetData<T>(cubeMapFace, data, 0, size * size);
        }

        /// <summary>
        /// Sets cube texture data, specifying a cubemap face, start index, and
        /// number of elements.
        /// </summary>
        /// <param name="cubeMapFace">The cubemap face.</param>
        /// <param name="data">Array of data.</param>
        /// <param name="startIndex">Index of the first element to set.</param>
        /// <param name="elementCount">Number of elements to set.</param>
        public void SetData<T>(CubeMapFace cubeMapFace, T[] data, int startIndex, int elementCount) where T : struct
        {
            // untested
            if (elementCount != size * size)
                throw new Exception("Data array is not the right size for texture data");
            if (startIndex < 0 || startIndex + elementCount > data.Length)
                throw new Exception("Index range does not lie within the data array");

            // Create a staging buffer to get CPU write access
            Texture2DDescription desc;
            SlimDX.Direct3D11.Texture2D dxstaging;
            desc = dxtexture.Description;
            desc.ArraySize = 1;
            desc.BindFlags = BindFlags.None;
            desc.CpuAccessFlags = CpuAccessFlags.Write;
            desc.MipLevels = 1;
            desc.OptionFlags = ResourceOptionFlags.None;
            desc.Usage = ResourceUsage.Staging;
            dxstaging = new SlimDX.Direct3D11.Texture2D(Game.GraphicsDevice.DX_Device, desc);

            // Write data to staging buffer
            DataBox box = Game.GraphicsDevice.DX_Device_Context.MapSubresource(dxstaging, 0, 0, MapMode.Write, MapFlags.None);
            box.Data.WriteRange<T>(data, startIndex, elementCount);
            Game.GraphicsDevice.DX_Device_Context.UnmapSubresource(dxstaging, 0);

            // Transfer data to the texturecube
            Game.GraphicsDevice.DX_Device_Context.CopySubresourceRegion(dxstaging, 0,
                                                                        new ResourceRegion(0, 0, 0, size, size, 1),
                                                                        dxtexture, (int)cubeMapFace, 0, 0, 0);

            // Dispose of the unmanaged staging buffer structure
            dxstaging.Dispose();
        }

        /// <summary>
        /// Sets cube texture data, specifying a cubemap face, mipmap level, source
        /// rectangle, start index, and number of elements.
        /// </summary>
        /// <param name="cubeMapFace">Cubemap face.</param>
        /// <param name="level">Mipmap level.</param>
        /// <param name="rect">Region in the texture to set the data; use null to set
        /// data to the entire texture.</param>
        /// <param name="data">Array of data.</param>
        /// <param name="startIndex">Index of the first element to set.</param>
        /// <param name="elementCount">Number of elements to set.</param>
        public void SetData<T>(CubeMapFace cubeMapFace, int level, Nullable<Rectangle> rect, T[] data, int startIndex, int elementCount) where T : struct
        {
            Texture2DDescription desc;
            int left = 0, top = 0, wide, high;
            SlimDX.Direct3D11.Texture2D dxstaging;
            desc = dxtexture.Description;
            desc.ArraySize = 1;
            Texture2D.CalculateLevelSize(level, size, size, out wide, out high);
            if (rect.HasValue)
            {
                left = rect.Value.Left;
                top = rect.Value.Top;
                if (left < 0 || left >= wide || top < 0 || top >= high || rect.Value.Width > wide || rect.Value.Height > high)
                    throw new Exception("Rectangle (" + top.ToString() + "," + left.ToString() + "," + rect.Value.Right.ToString() + "," + rect.Value.Bottom.ToString()
                                        + ") not inside mip level " + level.ToString() + " of TextureCube(" + wide.ToString() + "," + high.ToString() + ")");
                wide = rect.Value.Width;
                high = rect.Value.Height;
            }
            if (elementCount != wide * high)
                throw new Exception("Data array is not the right size for texture data");
            if (startIndex < 0 || startIndex + elementCount > data.Length)
                throw new Exception("Index range does not lie within the data array");
            desc.Width = wide;
            desc.Height = high;
            desc.MipLevels = 1;
            desc.BindFlags = BindFlags.None;
            desc.CpuAccessFlags = CpuAccessFlags.Write;
            desc.OptionFlags = ResourceOptionFlags.None;
            desc.Usage = ResourceUsage.Staging;
            dxstaging = new SlimDX.Direct3D11.Texture2D(Game.GraphicsDevice.DX_Device, desc);

            // Write data to staging buffer
            DataBox box = Game.GraphicsDevice.DX_Device_Context.MapSubresource(dxstaging, 0, 0, MapMode.Write, MapFlags.None);
            box.Data.WriteRange<T>(data, startIndex, elementCount);
            Game.GraphicsDevice.DX_Device_Context.UnmapSubresource(dxstaging, 0);

            // Transfer data to the texturecube
            Game.GraphicsDevice.DX_Device_Context.CopySubresourceRegion(dxstaging, 0,
                                                                        new ResourceRegion(0, 0, 0, wide, high, 1),
                                                                        dxtexture, (int)cubeMapFace, left, top, level);  // ?????

            // Dispose of the unmanaged staging buffer structure
            dxstaging.Dispose();
        }

        /// <summary>
        /// Gets a copy of cube texture data specifying a cubemap face.
        /// </summary>
        /// <param name="cubeMapFace">Cubemap face.</param>
        /// <param name="data">Array of data.</param>
        public void GetData<T>(CubeMapFace cubeMapFace, T[] data) where T : struct
        {
            GetData<T>(cubeMapFace, data, 0, size * size);
        }

        /// <summary>
        /// Gets a copy of cube texture data, specifying a cubemap face, start index,
        /// and number of elements.
        /// </summary>
        /// <param name="cubeMapFace">Cubemap face.</param>
        /// <param name="data">Array of data.</param>
        /// <param name="startIndex">Index of the first element to get.</param>
        /// <param name="elementCount">Number of elements to get.</param>
        public void GetData<T>(CubeMapFace cubeMapFace, T[] data, int startIndex, int elementCount) where T : struct
        {
            // Assuming here that the data comes from the top mip level
            if (elementCount != size * size)
                throw new Exception("Data array is not the right size for cube texture data");
            if (startIndex < 0 || startIndex + elementCount > data.Length)
                throw new Exception("Index range does not lie within the data array");

            // Create a staging buffer to get CPU read access
            Texture2DDescription desc;
            SlimDX.Direct3D11.Texture2D dxstaging;
            desc = dxtexture.Description;
            desc.ArraySize = 1;
            desc.BindFlags = BindFlags.None;
            desc.CpuAccessFlags = CpuAccessFlags.Read;
            desc.MipLevels = 1;
            desc.OptionFlags = ResourceOptionFlags.None;
            desc.Usage = ResourceUsage.Staging;
            // TODO: Assumes that T is Color - but don't know how to check that.  Should be ok though
            // as I don't provide any way of creating textures of any other element kind.
            dxstaging = new SlimDX.Direct3D11.Texture2D(Game.GraphicsDevice.DX_Device, desc);

            // Transfer data from the texture2d to the staging buffer
            // Note - CopyResource seems to require resources of exactly same size and depth, so we
            // must use CopySubresourceRegion to copy into the single mip level of the staging buffer
            Game.GraphicsDevice.DX_Device_Context.CopySubresourceRegion(dxtexture, (int)cubeMapFace,
                                                                        new ResourceRegion(0, 0, 0, size, size, 1),
                                                                        dxstaging, 0, 0, 0, 0);

            // Read data from staging buffer
            DataBox box = Game.GraphicsDevice.DX_Device_Context.MapSubresource(dxstaging, 0, 0, MapMode.Read, MapFlags.None);
            box.Data.ReadRange<T>(data, startIndex, elementCount);
            Game.GraphicsDevice.DX_Device_Context.UnmapSubresource(dxstaging, 0);

            // Dispose of the unmanaged staging buffer structure
            dxstaging.Dispose();
        }

        /// <summary>
        /// Gets a copy of cube texture data, specifying a cubemap face, mimap level,
        /// source rectangle, start index, and number of elements.
        /// </summary>
        /// <param name="cubeMapFace">Cube map face.</param>
        /// <param name="level">Mipmap level.</param>
        /// <param name="rect">The section of the texture where the data will be
        /// placed. null indicates the data will be copied over the entire texture.
        /// </param>
        /// <param name="data">Array of data.</param>
        /// <param name="startIndex">Index of the first element to get.</param>
        /// <param name="elementCount">Number of elements to get.</param>
        public void GetData<T>(CubeMapFace cubeMapFace, int level, Nullable<Rectangle> rect, T[] data, int startIndex, int elementCount) where T : struct
        {
            // Create a staging buffer to get CPU read access
            Texture2DDescription desc;
            int left = 0, top = 0, wide, high;
            SlimDX.Direct3D11.Texture2D dxstaging;
            desc = dxtexture.Description;
            desc.ArraySize = 1;
            Texture2D.CalculateLevelSize(level, size, size, out wide, out high);
            if (rect.HasValue)
            {
                left = rect.Value.Left;
                top = rect.Value.Top;
                if (left < 0 || left >= wide || top < 0 || top >= high || rect.Value.Width > wide || rect.Value.Height > high)
                    throw new Exception("Rectangle (" + top.ToString() + "," + left.ToString() + "," + rect.Value.Right.ToString() + "," + rect.Value.Bottom.ToString()
                                        + ") not inside mip level " + level.ToString() + " of Texture2D(" + wide.ToString() + "," + high.ToString() + ")");
                wide = rect.Value.Width;
                high = rect.Value.Height;
            }
            if (elementCount != wide * high)
                throw new Exception("Data array is not the right size for texture data");
            if (startIndex < 0 || startIndex + elementCount > data.Length)
                throw new Exception("Index range does not lie within the data array");
            desc.Width = wide;
            desc.Height = high;
            desc.MipLevels = 1;
            desc.BindFlags = BindFlags.None;
            desc.CpuAccessFlags = CpuAccessFlags.Read;
            desc.OptionFlags = ResourceOptionFlags.None;
            desc.Usage = ResourceUsage.Staging;
            // TODO: Assumes that T is Color - but don't know how to check that.  Should be ok though
            // as I don't provide any way of creating textures of any other element kind.
            dxstaging = new SlimDX.Direct3D11.Texture2D(Game.GraphicsDevice.DX_Device, desc);

            // Transfer data from the texture2d to the staging buffer
            // Note - CopyResource seems to require resources of exactly same size and depth, so we
            // must use CopySubresourceRegion to copy into the single mip level of the staging buffer
            Game.GraphicsDevice.DX_Device_Context.CopySubresourceRegion(dxtexture, (int)cubeMapFace,
                                                                        new ResourceRegion(left, top, level, left + wide, top + high, level + 1),
                                                                        dxstaging, 0, 0, 0, 0);

            // Read data from staging buffer
            DataBox box = Game.GraphicsDevice.DX_Device_Context.MapSubresource(dxstaging, 0, 0, MapMode.Read, MapFlags.None);
            box.Data.ReadRange<T>(data, startIndex, elementCount);
            Game.GraphicsDevice.DX_Device_Context.UnmapSubresource(dxstaging, 0);

            // Dispose of the unmanaged staging buffer structure
            dxstaging.Dispose();
        }

        public void Dispose()
        {
            if (dxtextureview != null) dxtextureview.Dispose();
            if (dxtextureview != null) dxtexture.Dispose();
        }

        //////////////////////////////////////////////////////////////////////////////////////////////////////////////
        ////////////////////////////////////////// JBBRXG11 Extensions ///////////////////////////////////////////////
        //////////////////////////////////////////////////////////////////////////////////////////////////////////////


        /// <summary>
        /// Sets cube texture data for a face, taking data from a Texture2D item of the same size
        /// </summary>
        /// <param name="cubeMapFace">The cubemap face.</param>
        /// <param name="sourcetex">The Texture2D providing the data</param>
        public void SetData(CubeMapFace cubeMapFace, Texture2D sourcetex)
        {
            // untested
            if (size * size != sourcetex.Width * sourcetex.Height)
                throw new Exception("Texture2D is not the right size to provide texture data");

            // Transfer data to the texturecube
            Game.GraphicsDevice.DX_Device_Context.CopySubresourceRegion(sourcetex.DXTexture, 0,
                                                                        new ResourceRegion(0, 0, 0, size, size, 1),
                                                                        dxtexture, (int)cubeMapFace, 0, 0, 0);
        }
    }
}

// Full XNA4 API in place
